package weather

// reference https://encore.dev/blog/advanced-go-concurrency

import (
	"context"
	"fmt"
	"golang.org/x/sync/errorgroup"
	"golang.org/x/sync/singleflight"
	"sync"
)

type Info struct {
	TempC, TempF int    // temperature in Celsius and Farenheit
	Conditions   string // "sunny", "snowing", etc
}

var group singleflight.Group

func City(city string) (*Info, error) {
	// The third value from group.Do, which is ignored in the example,
	// indicates whether the result was shared between multiple callers or not
	results, err, _ := group.Do(city, func() (interface{}, error) {
		info, err := fetchWeatherFromDb(city) // slow operation
		return info, err
	})
	if err != nil {
		return nil, fmt.Errorf("weather.City %s: %w", city, err)
	}
	return results.(*Info), nil
}

// Package errorgroup is useful when you have multiple operations that you want to wait for,
// but you also want to determine if they all completed successfully.

func Cities(cities ...string) ([]*Info, error) {
	var g errorgroup.Group
	var mu sync.Mutex

	res := make([]*Info, len(cities))
	for i, city := range cities {
		i, city := i, city // create locals for closure below

		// Method Go spawns a goroutine to run the task.
		g.Go(func() error {
			info, err := City(city)
			mu.Lock()
			res[i] = info
			mu.Unlock()
			return err
		})
	}
	// When you've spawned all the tasks you want, use Wait() to wait for them to complete.
	if err := g.Wait(); err != nil {
		return nil, err
	}
	// The error is nil if and only if all the tasks returned a nil error
	return res, nil
}

// We use semaphore here to keep track of how many tasks are running,
// and to block until there is room for another task to start.
// https://www.guru99.com/semaphore-in-operating-system.html
func Cities1(cities ...string) ([]*Info, error) {
	var g errorgroup.Group
	var mu sync.Mutex

	res := make([]*Info, len(cities))
	// allow up to 10 tasks to run at once
	sem := make(chan struct{}, 10)

	for i, city := range cities {
		i, city := i, city // create locals for closure below
		sem <- struct{}{}  // Acquire

		g.Go(func() error {
			info, err := City(city)
			mu.Lock()
			res[i] = info
			mu.Unlock()
			<-sem // Release
			return err
		})
	}

	if err := g.Wait(); err != nil {
		return nil, err
	}
	// The error is nil if and only if all the tasks returned a nil error
	return res, nil
}

// Weighted Bounded Concurrency
func Cities2(cities ...string) ([]*Info, error) {
	ctx := context.TODO() // replace with a real context
	var g errorgroup.Group
	var mu sync.Mutex

	res := make([]*Info, len(cities))
	sem := semaphore.NewWeighted(100)
	for i, city := range cities {
		i, city := i, city
		cost := int64(len(city)) // mock weight
		if err := sem.Acquire(ctx, cost); err != nil {
			break
		}
		g.Go(func() error {
			info, err := City(city)
			mu.Lock()
			res[i] = info
			mu.Unlock()
			sem.Release(cost)
			return err
		})
	}

	if err := g.Wait(); err != nil {
		return nil, err
	} else if err := ctx.Err(); err != nil {
		return nil, err
	}
	return res, nil
}
